﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SpaceSquirrel.SpriteRenderers;
using SpaceSquirrel.Automation;
using TuxedoLib;

namespace SpaceSquirrel
{
	public class Sprite
	{
		public string type;
		public bool isEnemy;
		public int height;
		public int x;
		public int y;
		public double modelX;
		public double modelY;
		private double[] xs;
		private double[] ys;
		public double vy;

		public bool moving;
		public string lastDirection;
		public bool onGround;
		private int[][] neighbors;
		public List<Sprite> spawns;
		public bool dead;
		public bool collideWall;
		public bool deleteWhenOffScreen;
		public bool ghost;
		private SpriteRenderer renderImpl;
		public SpriteAutomation automation;
		public bool floats;
		public bool sprinkle;
		public bool isBullet;
		public double bvx; // bullet VX
		public bool inBackground;
		public bool bikeMode;
		public PowerUp powerUpInfo;
		public string deathState = null;
		public double dx;
		public double dy;
		public double iceVX;
		public double ddx;
		public string damageDir;
		public bool cling;
		public double ladderDY;
		public int blinkCounter;
		public bool cameFromIce;

		// hack!
		public string acornTopDir;
		public bool goLeft;

		private static readonly int[][] XY_PAIRINGS = new int[][] { 
			new int[] { 0, 0 },
			new int[] { 1, 0 },
			new int[] { 2, 0 },
			new int[] { 3, 0 },
			new int[] { 4, 0 },
			new int[] { 4, 1 },
			new int[] { 4, 2 },
			new int[] { 4, 3 }
		};

		private static int GetSpriteHeight(string type)
		{
			if (type == "player_side" ||
				type == "lavamonster" ||
				type == "poof_big" ||
				type == "waterpop" ||
				type == "rocketbike")
			{
				return 32;
			}
			return 16;
		}

		private const double G = 0.7;
		private const double STRONG_G = 1.55;
		private const double WATER_G = 0.1;

		public void Update(PlayScene scene)
		{
			if (this.automation != null)
			{
				this.automation.DoStuff(scene);
			}

			this.blinkCounter--;
			if (this.blinkCounter < 0 || this.onGround)
			{
				this.ddx = 0;
				if (this.damageDir != null)
				{
					this.lastDirection = this.damageDir;
				}
				this.damageDir = null;
			}

			if (this.damageDir != null)
			{
				this.dx = 0;
			}

			this.dx += this.ddx;
			if (scene.side)
			{
				// hotspot is located in the center of the bottom most tile
				double areaX = this.modelX;
				double areaBottom = this.modelY + 8;
				int heightFromHotSpot = GetSpriteHeight(this.type) - 8 - 4;
				double areaTop = this.modelY - heightFromHotSpot;
				bool waterOnly = this.type == "moonsquid";

				int width = scene.cols;
				int height = scene.rows;

				bool isPlayer = this == scene.Player;
				bool bikePhysics = isPlayer && scene.id == "bike_level";

				if (bikePhysics)
				{
					int offsetX = -(scene.updateCounter * PlayScene.BIKE_SPEED);
					if (offsetX > 0) offsetX = 0;
					int right = -(scene.cols * 16 - 256);
					if (offsetX < right) offsetX = right;

					double newX = areaX + this.dx;
					double pixelX = newX + offsetX;
					if (pixelX < 8)
					{
						this.dx = PlayScene.BIKE_SPEED;
					}
					else if (pixelX > 248)
					{
						this.dx = PlayScene.BIKE_SPEED;
					}
				}

				int tileBottomIceCheck = (int)(areaBottom / 16);
				int tileX = (int)(areaX / 16);
				bool inWater = false;
				bool onIce = false;
				if (tileX >= 0 && tileBottomIceCheck > 0 && tileX < scene.cols && tileBottomIceCheck < scene.rows)
				{
					Tile tileBelow = scene.Tiles[tileX][tileBottomIceCheck];
					if (tileBelow != null)
					{
						onIce = tileBelow.isIce;
					}

					inWater = scene.Tiles[tileX][tileBottomIceCheck - 1].isWater;
				}

				// TODO: move to be constants
				double ICE_DIMINISH = .1;
				double ICE_MAX_SPEED = 8;

				if (this.type != "lazor" && (onIce || (this.cameFromIce && !this.onGround && !this.cling)))
				{
					if (this.dx == 0)
					{
						if (this.iceVX > 0)
						{
							this.iceVX -= ICE_DIMINISH;
							if (this.iceVX < 0)
							{
								this.iceVX = 0;
							}
						}
						else if (this.iceVX < 0)
						{
							this.iceVX += ICE_DIMINISH;
							if (this.iceVX > 0)
							{
								this.iceVX = 0;
							}
						}
					}
					else if (this.dx > 0)
					{
						this.iceVX += ICE_DIMINISH;
					}
					else if (this.dx < 0)
					{
						this.iceVX -= ICE_DIMINISH;
					}

					this.dx = this.iceVX;
					if (this.dx > ICE_MAX_SPEED)
					{
						this.dx = ICE_MAX_SPEED;
					}
					else if (this.dx < ICE_MAX_SPEED)
					{
						this.dx = -ICE_MAX_SPEED;
					}

					this.cameFromIce = true;
				}
				else
				{
					this.iceVX = 0;
					if (this.onGround)
					{
						this.cameFromIce = false;
					}
				}

				this.moving = this.dx != 0 || this.dy != 0;
				this.collideWall = false;
				// side-to-side calcuation is done first, independent of whether you are on the ground.
				if (this.dx != 0)
				{
					if (inWater && isPlayer)
					{
						double maxDX = this.onGround ? 1.0 : 1.5;
						if (this.dx < -maxDX)
						{
							this.dx = -maxDX;
						}
						else if (this.dx > maxDX)
						{
							this.dx = maxDX;
						}
					}

					double newX = this.modelX + this.dx;


					// isCollision ignores collisions near the sprite's feet
					// if you are near ground, you will be automatically placed standing
					// upon it at the end of the vertical adjustment phase.
					if (this.ghost || !scene.IsCollision(newX, areaTop, newX, areaBottom))
					{
						if (this.cling)
						{
							if ((int)(newX / 16) != ((int)(this.modelX / 16)))
							{
								newX = this.modelX;
							}
						}
						this.modelX = newX;
						if (this.dx > 0)
						{
							this.lastDirection = "right";
						}
						else
						{
							this.lastDirection = "left";
						}
					}
					else
					{
						this.iceVX = 0;
						this.collideWall = !this.ghost;
					}
					this.dx = 0;
				}

				// TODO: re-use of variable tileX. Probably a bad idea.
				tileX = (int)(this.modelX / 16);
				int tileY = (int)(this.modelY / 16);

				bool offScreen = tileX < 0 || tileX >= scene.cols || tileY < 0 || tileY >= scene.rows;
				bool onScreen = !offScreen;

				if (bikePhysics && this.collideWall)
				{
					scene.Next = new DeathOverrideScene(scene, "lava");
				}

				// vertical adjustment phase
				// assume sprite is flying through the air unless you see ground
				bool wasOnGround = this.onGround; // save the previous ground state. If you weren't on ground before but suddenly are, then you "landed" and a sound should be played. 
				this.onGround = false;

				if (wasOnGround || this.floats)
				{
					this.vy = 0;
				}
				else
				{
					if (inWater)
					{
						this.vy += WATER_G;
					}
					else if (scene.Context.Gravity)
					{
						this.vy += STRONG_G;
					}
					else
					{
						this.vy += G;
					}
				}

				bool rocketJump = false;
				if (inWater && isPlayer && this.vy < 0)
				{
					tileY = (int)(this.modelY / 16) - 1;
					if (tileY >= 0)
					{
						if (!scene.Tiles[tileX][tileY].isWater)
						{
							// moving up, bottom half is in water, top half is not
							rocketJump = true; // jump out with high velocity
						}
					}
				}

				// If you're clinging to a ladder, then throw out the vy entirely.
				// Use self.ladderDY instead
				if (this.cling)
				{
					this.dy = this.ladderDY;
					this.vy = 0;
					if (this.dy != 0)
					{
						this.moving = true;
					}
				}
				else
				{
					this.dy += this.vy;
				}

				double oldLadderDY = this.ladderDY;
				this.ladderDY = 0;

				double maxDY = 10;
				if (this.dy > 4 && inWater)
				{
					this.dy = 4;
				}

				if (this.dy > maxDY)
				{
					this.dy = maxDY;
				}
				else if (this.dy < -maxDY)
				{
					this.dy = -maxDY;
				}

				if (rocketJump)
				{
					this.dy = -15;
				}

				bool wasCling = this.cling;
				bool movedUp = false;

				double newBottom = areaBottom + this.dy;
				double newTop = areaTop + this.dy;

				bool no = false;

				if (this.ghost)
				{
					// just go with it
				}
				else
				{
					if (this.dy < 0)
					{
						// Going Up
						if (newTop < 0)
						{
							no = true;
						}
						else
						{
							int newTileTop = (int)(newTop / 16);
							offScreen = tileX < 0 || tileX >= scene.cols || newTileTop < 0 || newTileTop >= scene.rows;
							onScreen = !offScreen;

							if (onScreen)
							{
								Tile t = scene.Tiles[tileX][newTileTop];
								if (t.solid || (waterOnly && !t.isWater))
								{
									if (wasOnGround)
									{
										this.onGround = true;
									}
									no = true;

									if (this.modelY % 16 == 8)
									{
										no = false;
									}
									this.vy = 0;
									this.dy = 0;
									if (this == scene.Player) // isPlayer?
									{
										SoundManager.PlayNoise("head_bonk");
									}
								}
								else
								{
									movedUp = true;
								}
							}
							else
							{
								movedUp = true;
							}
						}
					}
					else
					{
						// Going Down (or staying the same)
						// The philosophy here is different. When you encounter a collision,
						// rather than stop the movement, you find where the top of the collided
						// tile is and place the sprite there.

						int newTileBottom = (int)(newBottom / 16);

						// If the player fell off the bottom
						if (isPlayer && newTileBottom >= height)
						{
							SoundManager.PlayNoise("fall_to_death");
							scene.Next = new DeathOverrideScene(scene, "fall");
							no = true;
						}
						else
						{
							offScreen = tileX < 0 || tileX >= scene.cols || newTileBottom < 0 || newTileBottom >= scene.rows;
							onScreen = !offScreen;

							if (onScreen)
							{
								Tile t = scene.Tiles[tileX][newTileBottom];
								bool topStop = false;
								if (t.isTop)
								{
									int oldTileBottom = (int)(areaBottom / 16);
									if (newTileBottom > oldTileBottom)
									{
										topStop = true;
									}
									else if (newTileBottom == oldTileBottom && areaBottom == newBottom)
									{
										topStop = true;
									}
								}
								if (t.solid || topStop)
								{
									no = true;
									this.cling = false;
									this.onGround = true;
									this.modelY = newTileBottom * 16 - 8;
									this.vy = 0;
									if (!wasOnGround && this == scene.Player)
									{
										SoundManager.PlayNoise("land_on_ground");
									}
								}
							}
							else
							{
								no = true;
							}
						}
					}
				}

				if (!no)
				{
					this.modelY = newBottom - 8;
				}

				this.x = (int)(this.modelX);
				this.y = (int)(this.modelY);
				this.dy = 0; // OOPS. This was probably supposed to be dx. Will fix after parity with python.
				this.dy = 0;

				tileY = (int)(this.modelY / 16);

				offScreen = tileX < 0 || tileX >= scene.cols || tileY < 0 || tileY >= scene.rows;
				onScreen = !offScreen;
				if (onScreen && scene.Tiles[tileX][tileY].isLadder)
				{
					// pass
				}
				else
				{
					this.cling = false;
				}

				if (movedUp && wasCling && !this.cling)
				{
					this.modelY = 16 * tileY; // maybe?
				}
			}
			else
			{
				this.moving = false;
				if (this.dx != 0 || this.dy != 0)
				{
					this.moving = true;

					if (this.dy < 0) 
						this.lastDirection = "up";
					else if (this.dy > 0) 
						this.lastDirection = "down";
					else if (this.dx < 0) 
						this.lastDirection = "left";
					else 
						this.lastDirection = "right";

					double[] xs = this.xs;
					double[] ys = this.ys;

					xs[0] = this.modelX + this.dx;
					xs[1] = this.modelX + this.dx * 3.0 / 4;
					xs[2] = this.modelX + this.dx / 2.0;
					xs[3] = this.modelX + this.dx / 4.0;
					xs[4] = this.modelX;

					ys[0] = this.modelY + this.dy;
					ys[1] = this.modelY + this.dy * 3 / 4;
					ys[2] = this.modelY + this.dy / 2;
					ys[3] = this.modelY + this.dy / 4;

					double newX, newY;
					int col, row;
					foreach (int[] pairing in Sprite.XY_PAIRINGS)
					{
						newX = xs[pairing[0]];
						newY = ys[pairing[1]];
						col = (int)(newX / 16);
						row = (int)(newY / 16);

						if (this.CheckNeighborCollisions(scene, col, row, newX, newY))
						{
							this.modelX = newX;
							this.modelY = newY;
							this.x = (int)this.modelX;
							this.y = (int)this.modelY;
							break;
						}
					}

					this.dx = 0;
					this.dy = 0;
				}
			}
		}

		public Sprite(string type, int px, int py)
		{
			this.type = type;
			this.height = Sprite.GetSpriteHeight(type);
			this.isEnemy = false;
			this.x = px;
			this.y = py;
			this.modelX = px;
			this.modelY = py;
			this.xs = new double[5];
			this.ys = new double[5];
			this.vy = 0;
			this.moving = false;
			this.lastDirection = "left";
			this.onGround = false;
			this.neighbors = new int[36][];
			this.spawns = null;
			this.dead = false;
			this.collideWall = false;
			this.deleteWhenOffScreen = false;
			this.ghost = false;
			this.renderImpl = SpriteRenderer.PLAYER_OVERWORLD;
			this.automation = null;
			this.floats = false;
			this.deathState = null;
			this.sprinkle = false;
			this.isBullet = type == "lazor";
			this.inBackground = false;
			this.bikeMode = false;
			this.powerUpInfo = null;

			switch (type)
			{
				case "acorn":
					this.renderImpl = SpriteRenderer.ACORN;
					this.automation = new AcornAutomation(this);
					this.isEnemy = true;
					break;

				case "acorntop":
					this.renderImpl = SpriteRenderer.ACORN_TOP;
					this.automation = new AcornTopAutomation(this);
					this.acornTopDir = "left";
					this.isEnemy = true;
					break;

				case "gravity_device":
					this.renderImpl = SpriteRenderer.GRAVITY_DEVICE;
					this.automation = null;
					break;

				case "lazor":
					this.renderImpl = SpriteRenderer.LAZOR;
					this.automation = new LazorAutomation(this);
					break;

				case "lavamonster":
					this.renderImpl = SpriteRenderer.LAVA_MONSTER;
					this.automation = new LavaMonsterAutomation(this);
					this.inBackground = true;
					this.isEnemy = true;
					this.floats = true;
					this.ghost = true;
					break;

				case "fireball":
					this.renderImpl = SpriteRenderer.FIREBALL;
					this.automation = new FireballAutomation(this);
					this.isEnemy = true;
					break;

				case "waterpop":
					this.renderImpl = SpriteRenderer.WATER_POP;
					this.automation = new WaterPopAutomation(this);
					this.floats = true;
					break;

				case "powerup":
					this.renderImpl = SpriteRenderer.POWER_UP;
					this.automation = new PowerUpAutomation(this);
					break;

				case "poof_big":
					this.renderImpl = SpriteRenderer.POOF;
					this.automation = new PoofAutomation(this, true);
					this.floats = true;
					break;

				case "poof_small":
					this.renderImpl = SpriteRenderer.POOF;
					this.automation = new PoofAutomation(this, false);
					this.floats = true;
					break;

				case "byat":
					this.renderImpl = SpriteRenderer.BYAT;
					this.automation = new ByatAutomation(this);
					this.floats = true;
					this.isEnemy = true;
					break;

				case "moonsquid":
					this.renderImpl = SpriteRenderer.MOON_SQUID;
					this.automation = new MoonSquidAutomation(this);
					this.isEnemy = true;
					break;

				case "seeds":
					this.renderImpl = SpriteRenderer.SEED;
					this.automation = new SeedAutomation(this);
					break;

				case "bird1":
				case "bird2":
					this.renderImpl = SpriteRenderer.BIRD;
					this.automation = new BirdAutomation(this, type == "bird2");
					this.floats = true;
					this.ghost = type == "bird2";
					this.isEnemy = true;
					break;

				case "rocketbike":
					this.renderImpl = SpriteRenderer.ROCKET_BIKE;
					this.automation = new RocketBikeAutomation(this);
					this.ghost = true;
					break;

				case "player_side":
				case "player_over":
					break;

				default:
					throw new InvalidOperationException("Invalid sprite type: " + type);
			}

			this.dx = 0;
			this.dy = 0;
			this.iceVX = 0;
			this.ddx = 0; // "damage dx", will stay set until you land on the ground or blink counter goes < 0
			this.damageDir = null;
			this.cling = false;
			this.ladderDY = 0;
			this.blinkCounter = -1;
			this.cameFromIce = false;
		}

		public bool IsCollision(Sprite other)
		{
			int left = this.x - 8;
			int oLeft = other.x - 8;
			int right = left + 16;
			int oRight = oLeft + 16;

			if (left >= oRight || right <= oLeft)
			{
				return false;
			}

			int bottom = this.y + 8;
			int oBottom = other.y + 8;
			int top = bottom - this.height;
			int oTop = oBottom - other.height;

			if (top >= oBottom || oTop >= bottom)
			{
				return false;
			}

			return true;
		}

		public void Render(PlayScene scene, Image screen, int offsetX, int offsetY, int renderCounter)
		{
			if (this.blinkCounter < 0 || (this.blinkCounter & 1) == 0)
			{
				this.renderImpl.Render(this, scene, screen, offsetX, offsetY, renderCounter);
			}
		}

		private bool CheckNeighborCollisions(PlayScene scene, int col, int row, double targetX, double targetY)
		{
			double areaLeft = targetX - 5;
			double areaRight = targetX + 5;
			double areaTop = targetY - 5;
			double areaBottom = targetY + 5;

			int width = scene.cols;
			int height = scene.rows;
			int left = col - 2;
			int right = col + 2;
			int top = row - 2;
			int bottom = row + 2;
			if (left < 0) left = 0;
			if (right >= width) right = width - 1;
			if (top < 0) top = 0;
			if (bottom >= height) bottom = height - 1;
			int index = 0;
			int x, y, csi;
			for (y = top; y <= bottom; ++y)
			{
				for (x = left; x <= right; ++x)
				{
					int[][] cs = scene.Tiles[x][y].collisions;
					for (csi = cs.Length - 1; csi >= 0; --csi)
					{
						this.neighbors[index] = cs[csi];
						index++;
					}
				}
			}

			--index;
			while (index >= 0)
			{
				int[] n = this.neighbors[index];
				if (n[0] > areaRight) { }
				else if (n[1] > areaBottom) { }
				else if (n[2] < areaLeft) { }
				else if (n[3] < areaTop) { }
				else { return false; }
				--index;
			}
			return true;
		}

		public void Hit(PlayScene scene, int amount)
		{
			this.blinkCounter = 40;
			scene.Context.AdjustHealth(scene, -amount);
			// I don't think this field is used anywhere...
			//this.blinkDirection = this.lastDirection;

			if (scene.id != "bike_level")
			{
				Tile[][] tiles = scene.Tiles;
				int tx = this.x / 16;
				int ty = this.y / 16;
				bool inWater = false;
				if (tx >= 0 && ty >= 0 && tx < scene.cols && ty < scene.rows)
				{
					Tile t = tiles[tx][ty];
					inWater = t.isWater;
				}

				this.damageDir = this.lastDirection;
				if (this.lastDirection == "left")
				{
					this.ddx = 3;
				}
				else
				{
					this.ddx = -3;
				}

				this.onGround = false;
				if (inWater)
				{
					this.vy = -1;
				}
				else
				{
					this.vy = -6;
				}
			}
		}
	}
}
